/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.db;

import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redora.exceptions.ConnectException;
import redora.util.ResourceFileHandler;

import java.io.File;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import static java.io.File.separator;
import static java.util.logging.Level.*;

/**
 * Central connection manager that connects to the database as set up in
 * /etc/redora/configuration.xml. This factory primarily loads and caches the
 * configuration file. Possible settings are:
 * <br>
 * database.default.host = 127.0.0.1<br>
 * database.default.database = test<br>
 * database.default.user = root<br>
 * database.default.password = <br>
 * database.default.runConfig = maxPerformance<br>
 * database.default.useUnicode = true<br>
 * database.default.characterEncoding = utf8<br>
 * database.default.autoReconnect = true<br>
 * database.default.autoCommit = true<br>
 *
 * @author Nanjing RedOrange (http://www.red-orange.cn)
 */
public class DatabaseFactory {

    static final transient Logger l = Logger.getLogger("redora.db.Database");

    final static Properties pDefault = new Properties();
    static {
        pDefault.setProperty("database.default.host", "127.0.0.1");
        pDefault.setProperty("database.default.database", "test");
        pDefault.setProperty("database.default.user", "root");
        pDefault.setProperty("database.default.password", "");
        pDefault.setProperty("database.default.runConfig", "maxPerformance");
        pDefault.setProperty("database.default.useUnicode", "true");
        pDefault.setProperty("database.default.characterEncoding", "utf8");
        pDefault.setProperty("database.default.autoReconnect", "true");
        pDefault.setProperty("database.default.autoCommit", "true");
    }
    /**
     * Local Database configuration file: <br>
     * /etc/redora/configuration.properties<br>
     * ~/redora/configuration.properties<br>
     * Or uses default values
     *
     */
    static final String LOCAL_CONFIGURATION_FILE = separator + "etc" + separator
            + "redora" + separator + "configuration.properties";

    static final Map<String, Properties> info = new HashMap<String, Properties>();
    static Properties p = null;
    static final Map<String, String> connect = new HashMap<String, String>();

    protected static Properties info(String schema) throws ConnectException {
        if (!info.containsKey(schema)) {
            info.put(schema, new Properties());
            String pre = "database." + schema;
            for (String key : new String[]{"user", "password", "runConfig", "useUnicode", "characterEncoding", "autoReconnect"}) {
                String value = getConfiguration().getProperty(pre + key);
                if (StringUtils.isNotEmpty(value))
                    info.get(schema).put(key, value);
                else
                    info.get(schema).put(key, getConfiguration().getProperty("database.default." + key));
            }
        }
        return info.get(schema);
    }

    @NotNull
    protected static String connect(@NotNull String schema) throws ConnectException {
        if (!connect.containsKey(schema)) {
            String host = getConfiguration().getProperty("database." + schema + ".host");
            if (StringUtils.isEmpty(host))
                host = getConfiguration().getProperty("database.default.host");
            String db = getConfiguration().getProperty("database." + schema + ".database");
            if (StringUtils.isEmpty(db))
                db = getConfiguration().getProperty("database.default.database");
            connect.put(schema, "jdbc:mysql://" + host + '/' + db);
        }
        return connect.get(schema);
    }

    private DatabaseFactory() { //avoid instantiation
    }

    /**
     * Searches for requested in: database.schema.key or database.default.key.
     * Because of the fallback to default, there is always a value for key. If not
     * an IllegalArgumentException is thrown.
     * @param schema (Mandatory)
     * @param key    (Mandatory)
     * @return
     * @throws ConnectException When there is an IOException when getting the properties file.
     */
    @NotNull
    public static String getDBProperty(@NotNull String schema, @NotNull String key)
            throws ConnectException {
        String retVal = getConfiguration().getProperty("database." + schema + '.' + key);
        if (StringUtils.isEmpty(retVal))
            retVal = getConfiguration().getProperty("database.default." + key);
        if (retVal == null)
            throw new IllegalArgumentException("The key " + key + " is not found in the properties schema or default. So, this key is invalid");
        return retVal;
    }

    /**
     * Loads and caches the configuration file.
     *
     * @return Connection details for DB connection \
     * @throws ConnectException On IOException loading the properties file
     */
    @NotNull
    static Properties getConfiguration() throws ConnectException {
        if (p == null) {
            l.log(INFO, "Loading configuration");
            p = new Properties(pDefault);
            try {
                File props = new File(System.getProperty("user.home") + separator + "redora"
                        + separator + "configuration.properties");
                if (props.exists()) {
                    Object[] sources = {props};
                    p.load(ResourceFileHandler.find(sources, null));
                    l.log(INFO, "Using {0}", props.getAbsolutePath());
                } else {
                    props = new File(LOCAL_CONFIGURATION_FILE);
                    if (props.exists()) {
                        Object[] sources = {props};
                        p.load(ResourceFileHandler.find(sources, null));
                        l.log(INFO, "Using {0}", props.getAbsolutePath());
                    } else {
                        l.log(INFO, "No configuration file found, I will use the defaults.");
                    }
                }
            } catch (IOException e) {
                l.log(SEVERE, "I could not load the configuration.properties for Redora", e);
                throw new ConnectException("I could not load the configuration.properties for Redora", e);
            }
        }
        return p;
    }

    /**
     * Creates a new Statement object with active Connection.
     *
     * @return Active statement
     * @throws ConnectException When a connection to te database appears to be impossible.
     */
    @NotNull
    public static Statement statement(@NotNull String schema) throws ConnectException {
        Statement ret = new Statement(new Connection(schema));
        try {
            ret.st = ret.con.con.createStatement();
        } catch (SQLException e) {
            l.log(WARNING, "Cannot initialize statement", e);
            throw new ConnectException("Cannot initialize statement", e);
        }
        return ret;
    }

    /**
     * Quietly closes the ResultSet.
     * @param rs (Optional) ResultSet that might br open
     */
    public static void close(@Nullable ResultSet rs) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            l.log(WARNING, "Failed to close result set", e);
        }
    }
}
